到昨日為止,大致依照第 15 日的規劃行進,但是目前還欠著 strip 實作的債;主要原因是 strip 當初不小心被歸類在小型程式裡面,但是它會涉及到 ELF 檔本身的改動,因此實作幅度實際上頗大。另外兩個小型程式的 size 和 nm 都已經直接實作完成。接下來我們先來介紹 objdump 工具程式吧!
既然今日主題是 objdump 介紹,就不得不提筆者以 binutils 工具包為目標的初衷了。筆者認為,binutils 工具包背負著許多過去的包袱,因此想要設計一個只針對 ELF 格式的系統二進位檔案工具。GNU binutils 工具包的部份程式各自提供一些實質重複的功能,筆者認為極不必要,而應該統合起來重新規劃。比方說我們目前為止已經完成的 readelf、size、以及 nm 三個工具,它們都是在閱讀 ELF 檔案,那麼為什麼不能整合成類似這樣的用法:
$ go-binutils show header # 類似 readelf -h
$ go-binutils show section-header # 類似 readelf -S
$ go-binutils show program-header # 類似 readelf -l
$ go-binutils show section-size # 類似 size
$ go-binutils show symbol # 類似 nm
這樣豈不是一目了然嗎?常常有許多 stackoverflow 網站上面的問題讓回答者很無奈,他們總會心想,你的這些問題,分明只要用 man 指令調查一下該程式的參數用法,不就找得到了嗎?這個想法,有它的正當性,但卻沒有理解到根本原因出在,POSIX 的命令列參數慣例(也就是我們其實也都已經很習以為常的 -s
、-a
之類單橫線內容)是有極限的,它們通常很難幫助人們記憶他們的功能:為什麼展示程式標頭的功能要用 -l
參數呢?GNU 應該有發現這個問題,因此推崇雙橫線參數增加可讀性的慣例,於是我們有了 --header
、--start-address
之類的參數,的確就比較方便在手冊中查找、也比較方便閱讀了。但是既然都已經決定要展示使用者所欲執行的真正動作,為什麼還需要那些礙事的橫線呢?各位喜歡看見 git --add
、git --commit
或是 docker --run
嗎?
UNIX 哲學裡面偏好工具應該體積小而數量多,如此便足以產生一些使用組合來圓滿使用者的真正需求。但如果當初真的可以透過 cat、diff、sed、awk 就兜得出版本控制的功能,我們就不需要版本控制工具了。所以筆者並不認為整合一個專為 ELF 格式服務的 binutils 程式會有什麼原則上的問題,因為事實證明這些工具分散又語意重複,筆者撰寫系列文期間也看了一些 stackoverflow 的問答,多得是發問者以為該用 objdump 看,實際上卻應該使用 readelf 取得資訊的例子。整合起來如上述展示內容的 sub-command 型操作方式,豈不是清爽得多?
那麼筆者為何還是要以 binutils 的順序與命名方式來實作功能?因為筆者認為數典忘祖也是不好的,應該要能夠提供類似
$ go-binutils legacy readelf $ go-binutils legacy size
之類的操作方式。
objdump 工具根據輸入參數的不同,能夠展示物件檔的不同部份內容,也有一些附屬參數能夠調整展示的範圍與特性等等。這裡介紹最常用的功能,也是筆者預計要實作的功能,就是 -d
參數的反組譯程式區段功能。
$ riscv64-unknown-linux-gnu-objdump -d a.out
...
0000000000010428 <main>:
10428: 1141 addi sp,sp,-16
1042a: e406 sd ra,8(sp)
1042c: e022 sd s0,0(sp)
1042e: 0800 addi s0,sp,16
10430: 4589 li a1,2
10432: 4505 li a0,1
10434: fe9ff0ef jal ra,1041c <add>
10438: 87aa mv a5,a0
1043a: 85be mv a1,a5
1043c: 67c1 lui a5,0x10
1043e: 4b078513 addi a0,a5,1200 # 104b0 <__libc_csu_fini+0x6>
10442: f0fff0ef jal ra,10350 <printf@plt>
10446: 4781 li a5,0
10448: 853e mv a0,a5
1044a: 60a2 ld ra,8(sp)
1044c: 6402 ld s0,0(sp)
1044e: 0141 addi sp,sp,16
10450: 8082 ret
...
以之前連結 add.s
與 main.c
的產物為例,這個指令會反組譯程式內容。這個 a.out 檔即會列出 .text 區段與 .plt 區段的內容(關於 PLT,我們在之後有機會會介紹)。之所以選上這兩個區段,是因為這兩個區段都有可執行內容的旗標被設置。使用之前的 readelf 工具觀察可以看到:
Number Name Type Flags
0 .shstrtab elf.SHT_STRTAB 0x0
1 .strtab elf.SHT_STRTAB 0x0
2 .symtab elf.SHT_SYMTAB 0x0
3 .debug_loc elf.SHT_PROGBITS 0x0
4 .debug_str elf.SHT_PROGBITS elf.SHF_MERGE+elf.SHF_STRINGS
5 .debug_frame elf.SHT_PROGBITS 0x0
6 .debug_line elf.SHT_PROGBITS 0x0
7 .debug_abbrev elf.SHT_PROGBITS 0x0
8 .debug_info elf.SHT_PROGBITS 0x0
9 .debug_aranges elf.SHT_PROGBITS 0x0
10 .comment elf.SHT_PROGBITS elf.SHF_MERGE+elf.SHF_STRINGS
11 .bss elf.SHT_NOBITS elf.SHF_WRITE+elf.SHF_ALLOC
12 .sdata elf.SHT_PROGBITS elf.SHF_WRITE+elf.SHF_ALLOC
13 .got elf.SHT_PROGBITS elf.SHF_WRITE+elf.SHF_ALLOC
14 .dynamic elf.SHT_DYNAMIC elf.SHF_WRITE+elf.SHF_ALLOC
15 .fini_array elf.SHT_FINI_ARRAY elf.SHF_WRITE+elf.SHF_ALLOC
16 .init_array elf.SHT_INIT_ARRAY elf.SHF_WRITE+elf.SHF_ALLOC
17 .preinit_array elf.SHT_PREINIT_ARRAY elf.SHF_WRITE+elf.SHF_ALLOC
18 .eh_frame elf.SHT_PROGBITS elf.SHF_ALLOC
19 .eh_frame_hdr elf.SHT_PROGBITS elf.SHF_ALLOC
20 .rodata elf.SHT_PROGBITS elf.SHF_ALLOC
21 .text elf.SHT_PROGBITS elf.SHF_ALLOC+elf.SHF_EXECINSTR
22 .plt elf.SHT_PROGBITS elf.SHF_ALLOC+elf.SHF_EXECINSTR
23 .rela.plt elf.SHT_RELA elf.SHF_ALLOC+elf.SHF_INFO_LINK
24 .gnu.version_r elf.SHT_GNU_VERNEED elf.SHF_ALLOC
25 .gnu.version elf.SHT_GNU_VERSYM elf.SHF_ALLOC
26 .dynstr elf.SHT_STRTAB elf.SHF_ALLOC
27 .dynsym elf.SHT_DYNSYM elf.SHF_ALLOC
28 .hash elf.SHT_HASH elf.SHF_ALLOC
29 .note.ABI-tag elf.SHT_NOTE elf.SHF_ALLOC
30 .interp elf.SHT_PROGBITS elf.SHF_ALLOC
31 elf.SHT_NULL 0x0
今天介紹 objdump 最常用的方法。明日我們會開始探討相關的實作如何進行。讀者諸君,明日再會!